﻿namespace Summoner
{
    using UnityEngine;
    using System.Collections.Generic;
    using System.IO;
    /*
        In this demo, we demonstrate:
        1.	Automatic asset bundle dependency resolving & loading.
            It shows how to use the manifest assetbundle like how to get the dependencies etc.
        2.	Automatic unloading of asset bundles (When an asset bundle or a dependency thereof is no longer needed, the asset bundle is unloaded)
        3.	Editor simulation. A bool defines if we load asset bundles from the project or are actually using asset bundles(doesn't work with assetbundle variants for now.)
            With this, you can player in editor mode without actually building the assetBundles.
        4.	Optional setup where to download all asset bundles
        5.	Build pipeline build postprocessor, integration so that building a player builds the asset bundles and puts them into the player data (Default implmenetation for loading assetbundles from disk on any platform)
        6.	Use WWW.LoadFromCacheOrDownload and feed 128 bit hash to it when downloading via web
            You can get the hash from the manifest assetbundle.
        7.	AssetBundle variants. A prioritized list of variants that should be used if the asset bundle with that variant exists, first variant in the list is the most preferred etc.
    */

    // Loaded assetBundle contains the references count which can be used to unload dependent assetBundles automatically.
    public class LoadedAssetBundle
    {
        public AssetBundle m_AssetBundle;
        public int m_ReferencedCount;

        public LoadedAssetBundle(AssetBundle assetBundle)
        {
            m_AssetBundle = assetBundle;
            m_ReferencedCount = 1;
        }
    }

    // Class takes care of loading assetBundle and its dependencies automatically, loading variants automatically.
    public class AssetBundleManager : MonoBehaviour
    {
        private static AssetBundleManager _instance = null;
        private string[] m_Variants = { };
        private AssetBundleManifest m_AssetBundleManifest = null;
        private Dictionary<string, LoadedAssetBundle> m_LoadedAssetBundles = new Dictionary<string, LoadedAssetBundle>();
        private Dictionary<string, LoadedAssetBundle> m_LoadedAssetBundlesSync = new Dictionary<string, LoadedAssetBundle>();
        private Dictionary<string, WWW> m_DownloadingWWWs = new Dictionary<string, WWW>();
        private Dictionary<string, string> m_DownloadingErrors = new Dictionary<string, string>();
        private List<AssetBundleLoadOperation> m_InProgressOperations = new List<AssetBundleLoadOperation>();
        private Dictionary<string, string[]> m_Dependencies = new Dictionary<string, string[]>();
        private Dictionary<string, string[]> m_DependenciesSync = new Dictionary<string, string[]>();
        private const string AssetBundleManifestName = "AssetBundleManifest";

        // Variants which is used to define the active variants.
        public string[] Variants
        {
            get { return m_Variants; }
            set { m_Variants = value; }
        }

        // AssetBundleManifest object which can be used to load the dependecies and check suitable assetBundle variants.
        public AssetBundleManifest AssetBundleManifestObject
        {
            set { m_AssetBundleManifest = value; }
        }

        // Get loaded AssetBundle, only return vaild object when all the dependencies are downloaded successfully.
        public LoadedAssetBundle GetLoadedAssetBundle(string assetBundleName, out string error)
        {
            if (m_DownloadingErrors.TryGetValue(assetBundleName, out error)) { return null; }
            LoadedAssetBundle bundle = null;
            m_LoadedAssetBundles.TryGetValue(assetBundleName, out bundle);
            if (bundle == null) { return null; }
            // No dependencies are recorded, only the bundle itself is required.
            string[] dependencies = null;
            if (!m_Dependencies.TryGetValue(assetBundleName, out dependencies)) { return bundle; }
            // Make sure all dependencies are loaded
            foreach (var dependency in dependencies)
            {
                if (m_DownloadingErrors.TryGetValue(assetBundleName, out error)) { return bundle; }
                // Wait all the dependent assetBundles being loaded.
                LoadedAssetBundle dependentBundle;
                m_LoadedAssetBundles.TryGetValue(dependency, out dependentBundle);
                if (dependentBundle == null) { return null; }
            }
            return bundle;
        }

        // Load AssetBundleManifest.
        public AssetBundleLoadManifestOperation Initialize(string manifestAssetBundleName)
        {
            LoadAssetBundle(manifestAssetBundleName, true);
            var operation = new AssetBundleLoadManifestOperation(manifestAssetBundleName, AssetBundleManifestName, typeof(AssetBundleManifest));
            m_InProgressOperations.Add(operation);
            return operation;
        }

        // Load AssetBundle and its dependencies.
        protected void LoadAssetBundle(string assetBundleName, bool isLoadingAssetBundleManifest = false)
        {
            if (!isLoadingAssetBundleManifest) { assetBundleName = RemapVariantName(assetBundleName); }
            // Check if the assetBundle has already been processed.
            bool isAlreadyProcessed = LoadAssetBundleInternal(assetBundleName, isLoadingAssetBundleManifest);
            // Load dependencies.
            if (!isAlreadyProcessed && !isLoadingAssetBundleManifest) { LoadDependencies(assetBundleName); }
        }

        // Remaps the asset bundle name to the best fitting asset bundle variant.
        protected string RemapVariantName(string assetBundleName)
        {
            string[] bundlesWithVariant = m_AssetBundleManifest.GetAllAssetBundlesWithVariant();
            // If the asset bundle doesn't have variant, simply return.
            if (System.Array.IndexOf(bundlesWithVariant, assetBundleName) < 0) { return assetBundleName; }
            string[] split = assetBundleName.Split('.');
            int bestFit = int.MaxValue;
            int bestFitIndex = -1;
            // Loop all the assetBundles with variant to find the best fit variant assetBundle.
            for (int i = 0; i < bundlesWithVariant.Length; i++)
            {
                string[] curSplit = bundlesWithVariant[i].Split('.');
                if (curSplit[0] != split[0]) { continue; }
                int found = System.Array.IndexOf(m_Variants, curSplit[1]);
                if (found != -1 && found < bestFit)
                {
                    bestFit = found;
                    bestFitIndex = i;
                }
            }
            if (bestFitIndex != -1)
            {
                return bundlesWithVariant[bestFitIndex];
            }
            else
            {
                return assetBundleName;
            }
        }

        private string GetLoadPathAsync(string assetBundleName)
        {
            if(File.Exists(Path.Combine(AssetPath.DocumentsPath, assetBundleName)))
            {
                return Path.Combine(AssetPath.DocumentsPath, assetBundleName);
            }
            else
            {
                if(AssetPath.IsEditor)
                {
                    return Path.Combine(AssetPath.PublishPath, assetBundleName);
                }
                else
                {
                    return Path.Combine(AssetPath.LoadAsyncPath, assetBundleName);
                }
            }
        }

        private string GetLoadPathSync(string assetBundleName)
        {
            if(File.Exists(Path.Combine(AssetPath.DocumentsPath, assetBundleName)))
            {
                return Path.Combine(AssetPath.DocumentsPath, assetBundleName);
            }
            else
            {
                if(AssetPath.IsEditor)
                {
                    return Path.Combine(AssetPath.PublishPath, assetBundleName);
                }
                else
                {
                    return Path.Combine(AssetPath.LoadSyncPath, assetBundleName);
                }
            }
        }

        // Where we actuall call WWW to download the assetBundle.
        protected bool LoadAssetBundleInternal(string assetBundleName, bool isLoadingAssetBundleManifest)
        {
            // Already loaded.
            LoadedAssetBundle bundle = null;
            m_LoadedAssetBundles.TryGetValue(assetBundleName, out bundle);
            if (bundle != null)
            {
                bundle.m_ReferencedCount++;
                return true;
            }
            // @TODO: Do we need to consider the referenced count of WWWs?
            // In the demo, we never have duplicate WWWs as we wait LoadAssetAsync()/LoadLevelAsync() to be finished before calling another LoadAssetAsync()/LoadLevelAsync().
            // But in the real case, users can call LoadAssetAsync()/LoadLevelAsync() several times then wait them to be finished which might have duplicate WWWs.
            if (m_DownloadingWWWs.ContainsKey(assetBundleName)) { return true; }
            string url = this.GetLoadPathAsync(assetBundleName);
            m_DownloadingWWWs.Add(assetBundleName, new WWW(url));
            return false;
        }

        // Where we get all the dependencies and load them all.
        protected void LoadDependencies(string assetBundleName)
        {
            if (m_AssetBundleManifest == null)
            {
                Debug.LogError("Please initialize AssetBundleManifest by calling AssetBundleManager.Initialize()");
                return;
            }
            // Get dependecies from the AssetBundleManifest object..
            string[] dependencies = m_AssetBundleManifest.GetAllDependencies(assetBundleName);
            if (dependencies.Length == 0) { return; }
            for (int i = 0; i < dependencies.Length; i++)
            {
                dependencies[i] = RemapVariantName(dependencies[i]);
            }
            // Record and load all dependencies.
            m_Dependencies.Add(assetBundleName, dependencies);
            for (int i = 0; i < dependencies.Length; i++)
            {
                LoadAssetBundleInternal(dependencies[i], false);
            }
        }

        // Unload assetbundle and its dependencies.
        public void UnloadAssetBundleAsync(string assetBundleName)
        {
            //Debug.Log(m_LoadedAssetBundles.Count + " assetbundle(s) in memory before unloading " + assetBundleName);
            UnloadAssetBundleInternal(assetBundleName);
            UnloadDependencies(assetBundleName);
            //Debug.Log(m_LoadedAssetBundles.Count + " assetbundle(s) in memory after unloading " + assetBundleName);
        }

        protected void UnloadDependencies(string assetBundleName)
        {
            string[] dependencies = null;
            if (!m_Dependencies.TryGetValue(assetBundleName, out dependencies)) { return; }
            // Loop dependencies.
            foreach (var dependency in dependencies)
            {
                UnloadAssetBundleInternal(dependency);
            }
            m_Dependencies.Remove(assetBundleName);
        }

        protected void UnloadAssetBundleInternal(string assetBundleName)
        {
            string error;
            LoadedAssetBundle bundle = GetLoadedAssetBundle(assetBundleName, out error);
            if (bundle == null) { return; }
            if (--bundle.m_ReferencedCount == 0)
            {
                bundle.m_AssetBundle.Unload(false);
                m_LoadedAssetBundles.Remove(assetBundleName);
                Debug.Log("AssetBundle " + assetBundleName + " has been unloaded successfully");
            }
        }

        void Update()
        {
            // Collect all the finished WWWs.
            var keysToRemove = new List<string>();
            foreach (var keyValue in m_DownloadingWWWs)
            {
                WWW download = keyValue.Value;
                // If downloading fails.
                if (download.error != null)
                {
                    m_DownloadingErrors.Add(keyValue.Key, download.error);
                    keysToRemove.Add(keyValue.Key);
                    continue;
                }
                // If downloading succeeds.
                if (download.isDone)
                {
                    //Debug.Log("Downloading " + keyValue.Key + " is done at frame " + Time.frameCount);
                    m_LoadedAssetBundles.Add(keyValue.Key, new LoadedAssetBundle(download.assetBundle));
                    keysToRemove.Add(keyValue.Key);
                }
            }

            // Remove the finished WWWs.
            foreach (var key in keysToRemove)
            {
                WWW download = m_DownloadingWWWs[key];
                m_DownloadingWWWs.Remove(key);
                download.Dispose();
            }

            // Update all in progress operations
            for (int i = 0; i < m_InProgressOperations.Count;)
            {
                if (!m_InProgressOperations[i].Update())
                {
                    m_InProgressOperations.RemoveAt(i);
                }
                else
                {
                    i++;
                }
            }
        }

        // Load asset from the given assetBundle.
        public AssetBundleLoadAssetOperation LoadAssetAsync(string assetBundleName, string assetName, System.Type type)
        {
            AssetBundleLoadAssetOperation operation = null;
            LoadAssetBundle(assetBundleName);
            operation = new AssetBundleLoadAssetOperationFull(assetBundleName, assetName, type);
            m_InProgressOperations.Add(operation);
            return operation;
        }

        public AssetBundle LoadAssetBundleSync(string assetBundleName)
        {
            this.LoadDependenciesSync(assetBundleName);
            if(!m_LoadedAssetBundlesSync.ContainsKey(assetBundleName))
            {
                m_LoadedAssetBundlesSync[assetBundleName] = new LoadedAssetBundle(AssetBundle.LoadFromFile(this.GetLoadPathSync(assetBundleName)));
            }
            else
            {
                m_LoadedAssetBundlesSync[assetBundleName].m_ReferencedCount++;
            }
            AssetBundle bundle = m_LoadedAssetBundlesSync[assetBundleName].m_AssetBundle;
            if (bundle != null)
            {
                string[] dependencies = null;
                if (!m_DependenciesSync.TryGetValue(assetBundleName, out dependencies)) { return bundle; }
                // Make sure all dependencies are loaded
                foreach (var dependency in dependencies)
                {
                    // Wait all the dependent assetBundles being loaded.
                    LoadedAssetBundle dependentBundle;
                    if(!m_LoadedAssetBundlesSync.TryGetValue(dependency, out dependentBundle))
                    {
                        m_LoadedAssetBundlesSync[dependency] = new LoadedAssetBundle(AssetBundle.LoadFromFile(Path.Combine(AssetPath.LoadSyncPath, dependency)));
                        if (m_LoadedAssetBundlesSync[dependency].m_AssetBundle == null) { return null; }
                    }
                }
            }
            return bundle;
        }

        // Where we get all the dependencies and load them all.
        protected void LoadDependenciesSync(string assetBundleName)
        {
            if (m_AssetBundleManifest == null)
            {
                Debug.LogError("Please initialize AssetBundleManifest by calling AssetBundleManager.Initialize()");
                return;
            }
            // Get dependecies from the AssetBundleManifest object..
            string[] dependencies = m_AssetBundleManifest.GetAllDependencies(assetBundleName);
            if (dependencies.Length == 0) { return; }
            for (int i = 0; i < dependencies.Length; i++)
            {
                dependencies[i] = RemapVariantName(dependencies[i]);
            }
            // Record and load all dependencies.
            m_DependenciesSync.Add(assetBundleName, dependencies);
        }

        // Unload assetbundle and its dependencies.
        public void UnloadAssetBundleSync(string assetBundleName)
        {
            //Debug.Log(m_LoadedAssetBundles.Count + " assetbundle(s) in memory before unloading " + assetBundleName);
            UnloadAssetBundleInternalSync(assetBundleName);
            UnloadDependenciesSync(assetBundleName);
            //Debug.Log(m_LoadedAssetBundles.Count + " assetbundle(s) in memory after unloading " + assetBundleName);
        }

        protected void UnloadDependenciesSync(string assetBundleName)
        {
            string[] dependencies = null;
            if (!m_DependenciesSync.TryGetValue(assetBundleName, out dependencies)) { return; }
            // Loop dependencies.
            foreach (var dependency in dependencies)
            {
                UnloadAssetBundleInternalSync(dependency);
            }
            m_DependenciesSync.Remove(assetBundleName);
        }

        protected void UnloadAssetBundleInternalSync(string assetBundleName)
        {
            LoadedAssetBundle bundle = GetLoadedAssetBundleSync(assetBundleName);
            if (bundle == null) { return; }
            if (--bundle.m_ReferencedCount == 0)
            {
                bundle.m_AssetBundle.Unload(false);
                m_LoadedAssetBundlesSync.Remove(assetBundleName);
                Debug.Log("AssetBundle " + assetBundleName + " has been unloaded successfully");
            }
        }

        // Get loaded AssetBundle, only return vaild object when all the dependencies are downloaded successfully.
        public LoadedAssetBundle GetLoadedAssetBundleSync(string assetBundleName)
        {
            LoadedAssetBundle bundle = null;
            m_LoadedAssetBundlesSync.TryGetValue(assetBundleName, out bundle);
            if (bundle == null) { return null; }
            // No dependencies are recorded, only the bundle itself is required.
            string[] dependencies = null;
            if (!m_DependenciesSync.TryGetValue(assetBundleName, out dependencies)) { return bundle; }
            // Make sure all dependencies are loaded
            foreach (var dependency in dependencies)
            {
                // Wait all the dependent assetBundles being loaded.
                LoadedAssetBundle dependentBundle;
                m_LoadedAssetBundlesSync.TryGetValue(dependency, out dependentBundle);
                if (dependentBundle == null) { return null; }
            }
            return bundle;
        }

        public static AssetBundleManager instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = Transform.FindObjectOfType<AssetBundleManager>();
                }
                if (_instance == null)
                {
                    _instance = new GameObject("AssetBundleManager").AddComponent<AssetBundleManager>();
                }
                return _instance;
            }
        }
    } // End of AssetBundleManager.
}